Why you should avoid multiline string literals in C# with Git
Recently I started using Continuous Integration (CI) for my open source C# projects on GitHub. I found a http://www.AppVeyor.com would provide me this for free for my open source projects. I setup a few of my projects on the AppVeyor’s CI.
Unfortunately, one of my projects, Rhyous.EasyXml, failed four out of ten unit tests on the CI server. This made no sense. I had the code checked out on a work desktop and a laptop and all ten tests passed in both places.
I had a string that my EasyXml code generates. I had the expected Xml in the following multiline string literal.
public string PrettyUtf8Xml = @"<?xml version=""1.0"" encoding=""UTF-8""?> <Person> <FirstName>John</FirstName> <MiddleName>Al Leon</MiddleName> <LastName>Doe</LastName> </Person>";
The test results were not helpful because the string results in the test output were identical.
Starting test execution, please wait... Passed TestMethodLinearize Failed TestMethodPretty Error Message: Assert.AreEqual failed. Expected:<<?xml version="1.0" encoding="UTF-8"?> <Person> <FirstName>John</FirstName> <MiddleName>Al Leon</MiddleName> <LastName>Doe</LastName> </Person>>. Actual:<<?xml version="1.0" encoding="UTF-8"?> <Person> <FirstName>John</FirstName> <MiddleName>Al Leon</MiddleName> <LastName>Doe</LastName> </Person>>. Stack Trace: at Rhyous.EasyXml.Tests.XmlTests.TestMethodPretty() in C:\projects\easyxml\src\Unit Tests\Rhyous.EasyXml.Tests\XmlTests.cs:line 102
My first guess was that somehow my UTF-8 vs UTF-16 code wasn’t working and I set up to figure out how to compare the strings in a way that shows me the difference. I quickly found a wonderful string extension method ShouldEqualWithDiff for Unit Tests by Phil Haack. Phil Haack’s extension method is extremely helpful because it provides a verticle character by character output of the string if the comparison fails.
This provided the following output and pointed the finger of the problem directly at Git. See the highlighted lines 59 and 60 below that show that characters 38 and 39 fail to match up.
Failed TestMethodPretty Error Message: Assert.AreEqual failed. Expected:<<?xml version="1.0" encoding="UTF-8"?> <Person> <FirstName>John</FirstName> <MiddleName>Al Leon</MiddleName> <LastName>Doe</LastName> </Person>>. Actual:<<?xml version="1.0" encoding="UTF-8"?> <Person> <FirstName>John</FirstName> <MiddleName>Al Leon</MiddleName> <LastName>Doe</LastName> </Person>>. Stack Trace: at Rhyous.EasyXml.Tests.StringExtensions.ShouldEqualWithDiff(String actualValue, String expectedValue, DiffStyle diffStyle, TextWriter output) in C:\projects\easyxml\src\Unit Tests\Rhyous.EasyXml.Tests\StringExtensions.cs:line 50 at Rhyous.EasyXml.Tests.StringExtensions.ShouldEqualWithDiff(String actualValue, String expectedValue) in C:\projects\easyxml\src\Unit Tests\Rhyous.EasyXml.Tests\StringExtensions.cs:line 12 at Rhyous.EasyXml.Tests.XmlTests.TestMethodPretty() in C:\projects\easyxml\src\Unit Tests\Rhyous.EasyXml.Tests\XmlTests.cs:line 102 Standard Output Messages: Idx Actual Expected ------------------------- 0 60 < 60 < 1 63 ? 63 ? 2 120 x 120 x 3 109 m 109 m 4 108 l 108 l 5 32 \u20; 32 \u20; 6 118 v 118 v 7 101 e 101 e 8 114 r 114 r 9 115 s 115 s 10 105 i 105 i 11 111 o 111 o 12 110 n 110 n 13 61 = 61 = 14 34 " 34 " 15 49 1 49 1 16 46 . 46 . 17 48 0 48 0 18 34 " 34 " 19 32 \u20; 32 \u20; 20 101 e 101 e 21 110 n 110 n 22 99 c 99 c 23 111 o 111 o 24 100 d 100 d 25 105 i 105 i 26 110 n 110 n 27 103 g 103 g 28 61 = 61 = 29 34 " 34 " 30 85 U 85 U 31 84 T 84 T 32 70 F 70 F 33 45 - 45 - 34 56 8 56 8 35 34 " 34 " 36 63 ? 63 ? 37 62 > 62 > * 38 13 \r 10 \n * 39 10 \n 60 < * 40 60 < 80 P * 41 80 P 101 e * 42 101 e 114 r * 43 114 r 115 s * 44 115 s 111 o * 45 111 o 110 n * 46 110 n 62 > * 47 62 > 10 \n * 48 13 \r 32 \u20; * 49 10 \n 32 \u20; * 50 32 \u20; 60 < * 51 32 \u20; 70 F * 52 60 < 105 i * 53 70 F 114 r * 54 105 i 115 s * 55 114 r 116 t * 56 115 s 78 N * 57 116 t 97 a * 58 78 N 109 m * 59 97 a 101 e * 60 109 m 62 > * 61 101 e 74 J * 62 62 > 111 o . . .
The cause is carriage returns. Why would AppVeyor’s tests have only \n while running the tests on any of my machines has \r\n? Yes, Git is the reason. Git normalizes carriage returns when you check in and check out your code. On a Windows box, \r\n is converted to \n on check-in. On checkout \n is converted to \r\n. When AppVeyor checks out my code, the conversion from \n to \r\n doesn’t occur.
So my options to fix this are these:
- Change Git to:
- use \r\n and not change line endings at all
- Change my code to be a single line string
I chose the second option. I did not want to mess with the Git settings. Different people could have difference Git settings and if anyone else forked my code, and ran the tests, I wanted them to work. So I changed my code. Now the string literal is on one line and the new lines are indicated with \r\n.
public string PrettyUtf8Xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<Person>\r\n <FirstName>John</FirstName>\r\n <MiddleName>Al Leon</MiddleName>\r\n <LastName>Doe</LastName>\r\n</Person>";
And now my Continuous Integration on AppVeyor is building and passing tests.
I have similar issue with xml.
For now for generating xml I use StringBuilder, so I can Append each xml element on separate line and it more readable then one-line xml string.
Also I use StringBuilder 'cause it's necessary for me add some values to generated xml.